﻿/*************************************************************************
 * Created by OhZee Creative, LLC
 * 2013
 * This code is licensed under Microsoft Public License (Ms-PL)
 * http://opensource.org/licenses/ms-pl
 * http://ozcontrols.codeplex.com/
 *************************************************************************/

namespace OhZeeCreative.UI.Controls
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using OhZeeCreative.Helpers; 

#if SILVERLIGHT
    using System.Windows.Controls;
    using System.Windows;
#endif

#if NETFX_CORE
    using OhZeeCreative.Windows8;
    using Windows.Foundation;
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
#endif

    [EditorBrowsableAttribute(EditorBrowsableState.Always)]
    public sealed class EvenLayoutPanel : Panel
    {
        private enum LayoutType
        {
            Fixed,
            InfiniteHorizontal,
            InfiniteVertical,
        };

        private const double FallbackAspectRatio = 1.0;

        private Size infiniteLayoutChildSize;
        private List<Rect> placements = new List<Rect>();

        public static readonly DependencyProperty AspectRatioProperty = DependencyProperty.Register("AspectRatio", typeof(double), typeof(EvenLayoutPanel), new PropertyMetadata(double.NaN, AspectRatioPropertyChanged));
        public static readonly DependencyProperty PaddingProperty = DependencyProperty.Register("Padding", typeof(Thickness), typeof(EvenLayoutPanel), new PropertyMetadata(new Thickness(), GenericRenderInvalidatingPropertyChanged));
        public static readonly DependencyProperty ItemMarginProperty = DependencyProperty.Register("ItemMargin", typeof(double), typeof(EvenLayoutPanel), new PropertyMetadata(0.0, GenericRenderInvalidatingPropertyChanged));
        public static readonly DependencyProperty PreferLandscapeProperty = DependencyProperty.Register("PreferLandscape", typeof(bool), typeof(EvenLayoutPanel), new PropertyMetadata(true, GenericRenderInvalidatingPropertyChanged));
        public static readonly DependencyProperty ArrangeRowsFirstProperty = DependencyProperty.Register("ArrangeRowsFirst", typeof(bool), typeof(EvenLayoutPanel), new PropertyMetadata(true, GenericRenderInvalidatingPropertyChanged));
        public static readonly DependencyProperty ForceInlineProperty = DependencyProperty.Register("ForceInline", typeof(bool), typeof(EvenLayoutPanel), new PropertyMetadata(false, GenericRenderInvalidatingPropertyChanged));
        public static readonly DependencyProperty CenterLastRowOrColumnProperty = DependencyProperty.Register("CenterLastRowOrColumn", typeof(bool), typeof(EvenLayoutPanel), new PropertyMetadata(true, GenericRenderInvalidatingPropertyChanged));


        public bool PreferLandscape
        {
            get { return (bool)GetValue(PreferLandscapeProperty); }
            set { SetValue(PreferLandscapeProperty, value); }
        }

        public bool ArrangeRowsFirst
        {
            get { return (bool)GetValue(ArrangeRowsFirstProperty); }
            set { SetValue(ArrangeRowsFirstProperty, value); }
        }

        public bool ForceInline
        {
            get { return (bool)GetValue(ForceInlineProperty); }
            set { SetValue(ForceInlineProperty, value); }
        }

        public bool CenterLastRowOrColumn
        {
            get { return (bool)GetValue(CenterLastRowOrColumnProperty); }
            set { SetValue(CenterLastRowOrColumnProperty, value); }
        }     

        public double AspectRatio
        {
            get { return (double)GetValue(AspectRatioProperty); }
            set { SetValue(AspectRatioProperty, value); }
        }
        
        public Thickness Padding
        {
            get { return (Thickness)GetValue(PaddingProperty); }
            set { SetValue(PaddingProperty, value); }
        }

        public double ItemMargin
        {
            get { return (double)GetValue(ItemMarginProperty); }
            set { SetValue(ItemMarginProperty, value); }
        }

        private static void GenericRenderInvalidatingPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            EvenLayoutPanel panel = (EvenLayoutPanel)obj;
            panel.Update();
        }

        private static void AspectRatioPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            EvenLayoutPanel panel = (EvenLayoutPanel)obj;

            if (!DependencyHelper.CoerceMinimum(panel, args, AspectRatioProperty, 0.0, inclusive: false))
            {
                panel.Update();
            }
        }

        private void Update()
        {   
            this.InvalidateMeasure();
        }


        // Creates even placement for Count rectangles with Margin around them. Rectangles may overlap.
        private static void InitializeEvenLayout(Rect rect, int itemCount, double margin, LayoutInfo layoutInfo, List<Rect> placements, bool rowFirstPlacement, bool centerLastRowOrColumn)
        {
            placements.Clear();
            placements.Capacity = itemCount;

            double usableWidth = rect.Width;
            double usableHeight = rect.Height;

            if (usableWidth < layoutInfo.Width || usableHeight < layoutInfo.Height)
            {
                return;
            }

            // We will leave a gap in between each column. For 3 columns: X---X---X
            double gapWidth = layoutInfo.Columns == 1 ? 0.0 : margin;
            // Similarly for every row.
            double gapHeight = layoutInfo.Rows == 1 ? 0.0 : margin;

            double deltaWidth = gapWidth + layoutInfo.Width;
            double deltaHeight = gapHeight + layoutInfo.Height;

            double lastRowXOffset = 0.0;
            double lastColumnYOffset = 0.0;            
            int itemsOnLast;

            if (rowFirstPlacement)
            {
                itemsOnLast = layoutInfo.Rows == 1 ? itemCount : itemCount - (layoutInfo.Rows - 1) * layoutInfo.Columns;
                if (centerLastRowOrColumn && itemsOnLast != layoutInfo.Columns)
                {
                    double lengthTakenByItemsAndGaps = layoutInfo.Width * itemsOnLast + (itemsOnLast == 1 ? 0.0 : gapWidth * (itemsOnLast - 1));
                    lastRowXOffset = (usableWidth - lengthTakenByItemsAndGaps).Halved();
                }
            }
            else
            {
                itemsOnLast = layoutInfo.Columns == 1 ? itemCount : itemCount - (layoutInfo.Columns - 1) * layoutInfo.Rows;
                if (centerLastRowOrColumn &= itemsOnLast != layoutInfo.Rows)
                {
                    double lengthTakenByItemsAndGaps = layoutInfo.Height * itemsOnLast + (itemsOnLast == 1 ? 0.0 : gapHeight * (itemsOnLast - 1));
                    lastColumnYOffset = (usableHeight - lengthTakenByItemsAndGaps).Halved();
                }
            }
            
            for (int i = 0; i < itemCount; i++)
            {
                int column;
                int row;

                if (!rowFirstPlacement)
                {
                    column = i / layoutInfo.Rows;
                    row = i - column * layoutInfo.Rows;
                }
                else
                {
                    row = i / layoutInfo.Columns;
                    column = i - row * layoutInfo.Columns;
                }
                                
                double xOffset = rect.X + deltaWidth * column;
                double yOffset = rect.Y + deltaHeight * row;

                if (i >= itemCount - itemsOnLast)
                {
                    xOffset += lastRowXOffset;
                    yOffset += lastColumnYOffset;
                }
                placements.Add(new Rect(xOffset, yOffset, layoutInfo.Width, layoutInfo.Height));
            }
        }

        private static void InitializeInlineLayout(Rect rect, int itemCount, double itemMargin, Size itemSize, List<Rect> placements, bool horizontal = true)
        {
            placements.Clear();
            placements.Capacity = itemCount;

            double usableWidth = rect.Width;
            double usableHeight = rect.Height;

            if (usableWidth < itemSize.Width || usableHeight < itemSize.Height)
            {
                return;
            }

            // We will leave a gap in between item. For 3 items in horizontal: X---X---X
            double gapLength = itemCount == 1 ? 0.0 : itemMargin;

            double deltaLength = gapLength + (horizontal ? itemSize.Width : itemSize.Height);

            for (int i = 0; i < itemCount; i++)
            {
#if SILVERLIGHT
                Rect placement = new Rect();
#endif
#if NETFX_CORE
                Rect placement;
#endif
                if (horizontal)
                {
                    placement.X = rect.X + deltaLength * i;
                    placement.Y = rect.Y;                    
                }
                else
                {
                    placement.X = rect.X;
                    placement.Y = rect.Y + deltaLength * i;
                }

                placement.Width = itemSize.Width;
                placement.Height = itemSize.Height;

                placements.Add(placement);
            }
        }


        private static void InitializeForcedInlineLayout(Rect rect, int itemCount, double itemMargin, double aspectRatio, List<Rect> placements, bool horizontal = true)
        {
#if SILVERLIGHT
            Size itemSize = new Size();
#endif
#if NETFX_CORE
            Size itemSize;
#endif

            if (horizontal)
            {
                itemSize.Height = rect.Height;
                if (double.IsNaN(aspectRatio) || aspectRatio == 0.0)
                {
                    itemSize.Width = itemCount <= 1 ? rect.Width : (rect.Width - itemMargin * (itemCount - 1)) / itemCount;
                }
                else
                {
                    itemSize.Width = itemSize.Height * aspectRatio;
                }
            }
            else
            {
                itemSize.Width = rect.Width;

                if (double.IsNaN(aspectRatio) || aspectRatio == 0.0)
                {
                    itemSize.Height = itemCount <= 1 ? rect.Height : (rect.Height - itemMargin * (itemCount - 1)) / itemCount;
                }
                else
                {
                    itemSize.Height = itemSize.Width / aspectRatio;
                }
            }
            
            InitializeInlineLayout(rect, itemCount, itemMargin, itemSize, placements, horizontal);
        }

        private Size DefaultMeasure()
        {
#if SILVERLIGHT
            Size zeroSize = new Size();
#endif
#if NETFX_CORE
            Size zeroSize;
#endif
            foreach (UIElement child in this.Children)
            {
                child.Measure(zeroSize);
            }
            return zeroSize;
        }

        private Size DefaultArrange()
        {
#if SILVERLIGHT
            Size zeroSize = new Size();
            Rect zeroRect = new Rect();
#endif
#if NETFX_CORE
            Size zeroSize;
            Rect zeroRect;
#endif
            foreach (UIElement child in this.Children)
            {
                child.Arrange(zeroRect);
            }
            return zeroSize;
        }

        // Tries to get the aspect ratio of the items whose width and height are specified in a way that the items are laid out as a square as much as possible
        // examples:
        // 1 | 1 2 | 1 2 | 1 2 | 1 2 3 | 1 2 3 | 1 2 3 |
        //         | 3   | 3 4 | 4 5   | 4 5 6 | 4 5 6 |
        //                                     | 7     |  etc.
        //
        private double GenerateEvenAspectRatio(double width, double height, int numberOfItems)
        {
            double sqrt = Math.Sqrt(numberOfItems);
            int rows, columns;

            if (Math.Floor(sqrt) == sqrt)
            {
                rows = columns = (int)sqrt;
            }
            else
            {
                if (this.PreferLandscape)
                {
                    columns = (int) Math.Ceiling(sqrt);
                    rows = (int)Math.Ceiling((double)numberOfItems / (double)columns);
                }
                else
                {
                    rows = (int) Math.Ceiling(sqrt);
                    columns = (int) Math.Ceiling((double)numberOfItems / (double)rows);
                }
            }

            double itemWidth = width / columns;
            double itemHeight = height / rows;

            return itemWidth / itemHeight;
        }
             
        private LayoutType layoutType;

        private const double HackLength = 0;

        protected override Size MeasureOverride(Size availableSize)
        {
            Size finalSize;
            double workingWidth;
            double workingHeight;

            this.layoutType = LayoutType.Fixed;

            int childrenCount = this.Children.Count;
            double aspectRatio = FallbackAspectRatio;

            if (childrenCount == 0)
            {
                return LayoutHelper.SafeSize(this.Padding.Left + this.Padding.Right, this.Padding.Top + this.Padding.Bottom);
            }

            if (double.IsInfinity(availableSize.Width) && double.IsInfinity(availableSize.Height))
            {
                workingWidth = this.Width;
                workingHeight = this.Height;

                if (double.IsNaN(workingWidth) && double.IsNaN(workingHeight))
                {
                    workingWidth = 0.0;
                    workingHeight = 0.0;
                }
                else if (double.IsNaN(workingWidth))
                {
                    workingWidth = 0.0;
                    this.layoutType = LayoutType.InfiniteHorizontal;
                }
                else if (double.IsNaN(workingHeight))
                {
                    workingHeight = 0.0;
                    this.layoutType = LayoutType.InfiniteVertical;
                }
            }
            else if (double.IsInfinity(availableSize.Width) && !double.IsInfinity(availableSize.Height))
            {
                this.layoutType = LayoutType.InfiniteHorizontal;
                workingHeight = Double.IsNaN(this.Height) ? availableSize.Height : this.Height;
                aspectRatio = double.IsNaN(this.AspectRatio) ? FallbackAspectRatio : this.AspectRatio;

                double height = workingHeight - (this.Padding.Top + this.Padding.Bottom);
                double width =  height * aspectRatio;

                this.infiniteLayoutChildSize = LayoutHelper.SafeSize(width, height);

                workingWidth = LayoutHelper.GetLength(width, childrenCount, this.ItemMargin) + this.Padding.Left + this.Padding.Right;
            }
            else if (!double.IsInfinity(availableSize.Width) && double.IsInfinity(availableSize.Height))
            {
                this.layoutType = LayoutType.InfiniteVertical;
                workingWidth = Double.IsNaN(this.Width) ? availableSize.Width : this.Width;
                aspectRatio = double.IsNaN(this.AspectRatio) ? FallbackAspectRatio : this.AspectRatio;

                double width = workingWidth - (this.Padding.Left + this.Padding.Right);
                double height = width  / aspectRatio;

                this.infiniteLayoutChildSize = LayoutHelper.SafeSize(width, height);

                workingHeight = LayoutHelper.GetLength(this.infiniteLayoutChildSize.Height, childrenCount, this.ItemMargin) + this.Padding.Bottom + this.Padding.Top;
            }
            else
            {
                workingWidth = Double.IsNaN(this.Width) ? availableSize.Width : Math.Min(this.Width, availableSize.Width);
                workingHeight = Double.IsNaN(this.Height) ? availableSize.Height : Math.Min(this.Height, availableSize.Height);

                aspectRatio = double.IsNaN(this.AspectRatio) ? this.GenerateEvenAspectRatio(workingWidth, workingHeight, childrenCount) : this.AspectRatio;
            }

            DebugHelper.Assert(workingWidth.IsValid() && workingHeight.IsValid() && aspectRatio.IsValid());

            finalSize = LayoutHelper.SafeSize(workingWidth, workingHeight);

            double useableWidth = finalSize.Width - (this.Padding.Left + this.Padding.Right);
            double useableHeight = finalSize.Height - (this.Padding.Top + this.Padding.Bottom);            
                
            if (useableWidth <= 0 || useableHeight <= 0)
            {
                foreach (UIElement child in this.Children)
                {
                    child.Measure(finalSize);
                }
                    
                return finalSize;
            }
#if SILVERLIGHT
            Rect useableSpace = new Rect(0, 0, useableWidth, useableHeight);
#endif
#if NETFX_CORE
            Rect useableSpace;
            useableSpace.Width = useableWidth;
            useableSpace.Height = useableHeight;
#endif

            if (this.layoutType != LayoutType.Fixed)
            {
                InitializeInlineLayout(useableSpace, childrenCount, this.ItemMargin, this.infiniteLayoutChildSize, this.placements, this.layoutType == LayoutType.InfiniteHorizontal);
            }
            else if (this.ForceInline)
            {  
                // Passing in the specified aspect ratio if any, else the aspect ratio will be determined by the function
                InitializeForcedInlineLayout(useableSpace, childrenCount, this.ItemMargin, this.AspectRatio, this.placements, this.PreferLandscape);
            }
            else
            {
                
                LayoutInfo layoutInfo = LayoutHelper.GetOptimumSize(useableSpace, aspectRatio, childrenCount, this.PreferLandscape, this.ItemMargin);
                if (layoutInfo == null)
                {
                    DebugHelper.Fail();
                    this.DefaultMeasure();
                    return finalSize;
                }

                InitializeEvenLayout(useableSpace, childrenCount, this.ItemMargin, layoutInfo, this.placements, this.ArrangeRowsFirst, this.CenterLastRowOrColumn);
            }

            if (this.placements.Count != childrenCount)
            {
                DebugHelper.Fail();
                this.DefaultMeasure();
                return finalSize;
            }

            double maxWidth = 0.0;
            double maxHeight = 0.0;

            for (int index = 0; index < childrenCount; index++)
            {
                UIElement element = this.Children[index];

                Rect rect = this.placements[index];
                rect.X += this.Padding.Left;
                rect.Y += this.Padding.Top;

                try
                {
                    element.Measure(rect.Size());
                }
                catch (Exception ex)
                {
                    string message = string.Format (
                        "EvenLayoutPanel failed MeasureOverride for element {0}. This issue may indicate a possible XAML problem (e.g. missing styles/resources or invalid binding) in this element. Please check the Debug Output window for any details.",
                        element.ToString());

                    throw new InvalidOperationException(message, ex);
                }

                maxWidth = Math.Max(maxWidth, rect.X + rect.Width);
                maxHeight = Math.Max(maxHeight, rect.Y + rect.Height);
            }
                            
            return LayoutHelper.SafeSize(maxWidth + this.Padding.Right, maxHeight + this.Padding.Bottom);
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            int childrenCount = this.Children.Count;

            if (childrenCount == 0)
            {
                return LayoutHelper.SafeSize(this.Padding.Left + this.Padding.Right, this.Padding.Top + this.Padding.Bottom);
            }
            
            double useableWidth = finalSize.Width - (this.Padding.Left + this.Padding.Right);
            double useableHeight = finalSize.Height - (this.Padding.Top + this.Padding.Bottom);
            
            if (useableWidth <= 0 || useableHeight <= 0)
            {
                this.DefaultArrange();
                return finalSize;
            }

            double aspectRatio = this.AspectRatio;
            
            if (double.IsNaN(aspectRatio))
            {
                aspectRatio = this.layoutType == LayoutType.Fixed ? this.GenerateEvenAspectRatio(useableWidth, useableHeight, childrenCount) : FallbackAspectRatio;
            }

            if (this.layoutType != LayoutType.Fixed)
            {
                this.infiniteLayoutChildSize.Width = this.layoutType == LayoutType.InfiniteHorizontal ? useableHeight * aspectRatio : useableWidth;
                this.infiniteLayoutChildSize.Height = this.layoutType == LayoutType.InfiniteVertical ? useableWidth / aspectRatio : useableHeight;                
            }

#if SILVERLIGHT
            Rect useableSpace = new Rect(0, 0, useableWidth, useableHeight);
#endif
#if NETFX_CORE
            Rect useableSpace;
            useableSpace.Width = useableWidth;
            useableSpace.Height = useableHeight;
#endif
            
            if (this.layoutType != LayoutType.Fixed)
            {
                InitializeInlineLayout(useableSpace, childrenCount, this.ItemMargin, this.infiniteLayoutChildSize, this.placements, this.layoutType == LayoutType.InfiniteHorizontal);
            }
            else if (this.ForceInline)
            {  
                // Passing in the specified aspect ratio if any, else the aspect ratio will be determined by the function
                InitializeForcedInlineLayout(useableSpace, childrenCount, this.ItemMargin, this.AspectRatio, this.placements, this.PreferLandscape);
            }
            else
            {
                LayoutInfo layoutInfo = LayoutHelper.GetOptimumSize(useableSpace, aspectRatio, childrenCount, this.PreferLandscape, this.ItemMargin);
                if (layoutInfo == null)
                {
                    DebugHelper.Fail();
                    this.DefaultArrange();
                    return finalSize;
                }

                InitializeEvenLayout(useableSpace, childrenCount, this.ItemMargin, layoutInfo, this.placements, this.ArrangeRowsFirst, this.CenterLastRowOrColumn);
            }

            if (this.placements.Count != childrenCount)
            {
                DebugHelper.Fail();
                this.DefaultArrange();
                return finalSize;
            }

            double maxWidth = 0.0;
            double maxHeight = 0.0;

            for (int index = 0; index < childrenCount; index++)
            {
                UIElement element = this.Children[index];
                                                
                Rect rect = this.placements[index];
                rect.X += this.Padding.Left;
                rect.Y += this.Padding.Top;

                element.Arrange(rect);

                maxWidth = Math.Max(maxWidth, rect.X + rect.Width);
                maxHeight = Math.Max(maxHeight, rect.Y + rect.Height);
            }

#if SILVERLIGHT
            Size realSize = new Size(maxWidth + this.Padding.Right, maxHeight + this.Padding.Bottom);
#endif
#if NETFX_CORE
            Size realSize;
            realSize.Width = maxWidth + this.Padding.Right;
            realSize.Height = maxHeight + this.Padding.Bottom;
#endif
            return realSize;
        }
    }
}
